// Copyright 2018 The Go Authors. All rights reserved.
// Use of this session code is governed by a BSD-style
// license that can be found in the LICENSE file.

package session

import (
	"context"
	"errors"
	"fmt"
	"log"
	"mi/compiler/ast"
	"mi/compiler/checker"
	"mi/compiler/scanner"
	"mi/compiler/token"
	"mi/internal/span"
	"mi/internal/system"
	"mi/internal/xpath"
)

type JSDocTagInfo struct {
	Name string
	Text string
}

type SymbolDisplayPart struct {
	Text string
	Kind string // A ScriptElementKind
}

type SignatureHelpParameter struct {
	name string
	documentation []SymbolDisplayPart
	displayParts []SymbolDisplayPart
	isOptional bool
}

/**
 * Represents a single signature to show in signature help.
 * The id is used for subsequent calls into the language service to ask questions about the
 * signature help item in the context of any documents that have been updated.  i.e. after
 * an edit has happened, while signature help is still active, the host can ask important
 * questions like 'what parameter is the user currently contained within?'.
 */
type SignatureHelpItem struct {
	IsVariadic bool
	PrefixDisplayParts []SymbolDisplayPart
	SuffixDisplayParts []SymbolDisplayPart
	SeparatorDisplayParts []SymbolDisplayPart
	Parameters SignatureHelpParameter
	Documentation []SymbolDisplayPart
	Tags []JSDocTagInfo
}

/**
 * Represents a parameter of a callable-signature. A parameter can
 * have a label and a doc-comment.
 */
type ParameterInformation struct {
	Label string
	Documentation string
}

/**
 * Represents the signature of something callable. A signature
 * can have a label, like a function-name, a doc-comment, and
 * a set of parameters.
 */
type SignatureInformation struct {
	Label string
	Documentation string
	Parameters []ParameterInformation
	ActiveParameter int
}


/**
 * Represents a set of signature help items, and the preferred item that should be selected.
 */
type SignatureHelpItems struct {
	Items             []SignatureInformation
	ApplicableSpan    span.UTF16Span
	SelectedItemIndex int
	ArgumentIndex     int
	ArgumentCount     int
}


type ArgumentListKind int

const (
	TypeArguments ArgumentListKind = iota
	CallArguments
)

type ArgumentListInfo struct {
	Kind ArgumentListKind
	Invocation ast.Node
	ArgumentsSpan span.UTF16Span
	ArgumentIndex int
	ArgumentCount int
}

func (s *session) GetSignatureHelpItems(_ context.Context, uri xpath.URI, pos span.UTF16Position) (*SignatureHelpItems, error) {
	s.programMux.Lock()
	defer s.programMux.Unlock()

	var sourceFile = s.program.GetSourceFile(uri)
	if sourceFile == nil {
		return nil, errors.New("Unknown file " + uri.Filename())
	}

	// Convert utf16 pos to byte offset
	offset, err := pos.ToOffsetWithCache(sourceFile.Text, sourceFile.GetLineStarts())
	if err != nil {
		return nil, err
	}

	var start = system.CurrentMillisecond()
	// Decide whether to show signature help
	var startingToken = findTokenOnLeftOfPosition(sourceFile, offset)
	if startingToken == nil {
		// We are at the beginning of the file
		return nil, nil
	}

	var argumentInfo = getContainingArgumentInfo(startingToken, offset, sourceFile)
	// Semantic filtering of signature help
	if argumentInfo == nil {
		return nil, nil
	}

	var call = argumentInfo.Invocation
	var typeChecker = s.program.GetTypeChecker()
	resolvedSignature, candidates := typeChecker.GetResolvedSignature(call)
	var items = createSignatureHelpItems(candidates, resolvedSignature, argumentInfo, typeChecker)
	log.Printf("getSignatureHelpItems: %d\n", system.CurrentMillisecond() - start)
	return items, nil
}

/**
* Returns relevant information for the argument list and the current argument if we are
* in the argument of an invocation; returns undefined otherwise.
*/
func getImmediatelyContainingArgumentInfo(node ast.Node, position int, sourceFile *ast.SourceFile) *ArgumentListInfo {
	if ast.IsCallExpression(node.Parent()) || ast.IsConstructorExpression(node.Parent()) {
		var callExpression = node.Parent()
		// There are 3 cases to handle:
		//   1. The token introduces a list, and should begin a sig help session
		//   2. The token is either not associated with a list, or ends a list, so the session should end
		//   3. The token is buried inside a list, and should give sig help
		//
		// The following are examples of each:
		//
		//    Case 1:
		//          foo<#T, U>(#a, b)    -> The token introduces a list, and should begin a sig help session
		//    Case 2:
		//          fo#o<T, U>#(a, b)#   -> The token is either not associated with a list, or ends a list, so the session should end
		//    Case 3:
		//          foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give sig help
		// Find out if 'ast' is an argument, a type argument, or neither
		if ast.IsTokenNodeAndEqualKind(node, token.LessThan) ||
			ast.IsTokenNodeAndEqualKind(node, token.OpenParen) {
			// Find the list that starts right *after* the < or ( token.
			// If the user has just opened a list, consider this item 0.
			var list = getChildListThatStartsWithOpenerToken(callExpression, node, sourceFile)
			var isTypeArgList bool
			switch pn := node.Parent().(type) {
			case *ast.CallExpression:
				isTypeArgList = pn.TypeArguments != nil && pn.TypeArguments.Pos() == list.Pos()
			case *ast.ConstructorExpression:
				isTypeArgList = pn.TypeArguments != nil && pn.TypeArguments.Pos() == list.Pos()
			}

			assert(list != nil)

			var kind ArgumentListKind
			if isTypeArgList {
				kind = TypeArguments
			} else {
				kind = CallArguments
			}

			return &ArgumentListInfo{
				Kind:          kind,
				Invocation:    callExpression,
				ArgumentsSpan: getApplicableSpanForArguments(list, sourceFile),
				ArgumentIndex: 0,
				ArgumentCount: getArgumentCount(list),
			}
		}

		// findListItemInfo can return undefined if we are not in parent's argument list
		// or type argument list. This includes cases where the cursor is:
		//   - To the right of the closing paren, non-substitution template, or template tail.
		//   - Between the type arguments and the arguments (greater than token)
		//   - On the target of the call (parent.func)
		//   - On the 'new' keyword in a 'new' expression
		var listItemInfo = findListItemInfo(node)
		if listItemInfo != nil {
			var list = listItemInfo.List
			var isTypeArgList bool
			switch pn := node.Parent().(type) {
			case *ast.CallExpression:
				isTypeArgList = pn.TypeArguments != nil && pn.TypeArguments.Pos() == list.Pos()
			case *ast.ConstructorExpression:
				isTypeArgList = pn.TypeArguments != nil && pn.TypeArguments.Pos() == list.Pos()
			}

			var argumentIndex = getArgumentIndex(list, node)
			var argumentCount = getArgumentCount(list)

			if !(argumentIndex == 0 || argumentIndex < argumentCount) {
				panic(fmt.Sprintf("argumentCount < argumentIndex, %d < %d", argumentCount, argumentIndex))
			}

			var kind ArgumentListKind
			if isTypeArgList {
				kind = TypeArguments
			} else {
				kind = CallArguments
			}

			return &ArgumentListInfo{
				Kind:          kind,
				Invocation:    callExpression,
				ArgumentsSpan: getApplicableSpanForArguments(list, sourceFile),
				ArgumentIndex: argumentIndex,
				ArgumentCount: argumentCount,
			}
		}
		return nil
	}

	return nil
}

func getArgumentIndex(argumentsList ast.Node, node ast.Node) int {
	// The list we got back can include commas.  In the presence of errors it may
	// also just have nodes without commas.  For example "Foo(a b c)" will have 3
	// args without commas.   We want to find what index we're at.  So we count
	// forward until we hit ourselves, only incrementing the index if it isn't a
	// comma.
	//
	// Note: the subtlety around trailing commas (in getArgumentCount) does not apply
	// here.  That's because we're only walking forward until we hit the ast we're
	// on.  In that case, even if we're after the trailing comma, we'll still see
	// that trailing comma in the list, and we'll have generated the appropriate
	// arg index.
	var argumentIndex = 0
	var listChildren = getNodeChildren(argumentsList, nil)
	for _, child := range listChildren {
		if child == node {
			break
		}

		if !ast.IsTokenNodeAndEqualKind(child, token.Comma) {
			argumentIndex++
		}
	}

	return argumentIndex
}

func getArgumentCount(argumentsList ast.Node) int {
	// The argument count for a list is normally the number of non-comma children it has.
	// For example, if you have "Foo(a,b)" then there will be three children of the arg
	// list 'a' '<comma>' 'b'.  So, in this case the arg count will be 2.  However, there
	// is a small subtlety.  If you have  "Foo(a,)", then the child list will just have
	// 'a' '<comma>'.  So, in the case where the last child is a comma, we increase the
	// arg count by one to compensate.
	//
	// Note: this subtlety only applies to the last comma.  If you had "Foo(a,,"  then
	// we'll have:  'a' '<comma>' '<missing>'
	// That will give us 2 non-commas.  We then add one for the last comma, givin us an
	// arg count of 3.

	var listChildren = getNodeChildren(argumentsList, nil)

	var argumentCount int
	for _, arg := range listChildren {
		if !ast.IsTokenNodeAndEqualKind(arg, token.Comma) {
			argumentCount++
		}
	}

	if len(listChildren) > 0 && ast.IsTokenNodeAndEqualKind(listChildren[len(listChildren)-1], token.Comma) {
		argumentCount++
	}

	return argumentCount
}

func getApplicableSpanForArguments(argumentsList ast.Node, sourceFile *ast.SourceFile) span.UTF16Span {
	// We use full start and skip trivia on the end because we want to include trivia on
	// both sides. For example,
	//
	//    foo(   /*comment */     a, b, c      /*comment*/     )
	//        |                                               |
	//
	// The applicable span is from the first bar to the second bar (inclusive,
	// but not including parentheses)


	var applicableSpanStart = getNodeFullStart(argumentsList)
	var applicableSpanEnd = scanner.SkipTrivia(sourceFile.Text, getNodeEnd(argumentsList), false, false)

	u16span, _ := span.UTF16SpanFromOffsetWithCache(applicableSpanStart,
		applicableSpanEnd, sourceFile.Text, sourceFile.GetLineStarts())
	return u16span
}

func getContainingArgumentInfo(node ast.Node, position int, sourceFile *ast.SourceFile) *ArgumentListInfo {
	for n := node;!ast.IsPackage(n); n = n.Parent() {
		if ast.IsFunctionBlock(n) {
			return nil
		}

		// If the ast is not a subspan of its parent, this is a big problem.
		// There have been crashes that might be caused by this violation.
		if !ast.IsSourceFile(n) && (n.Pos() < n.Parent().Pos() || n.End() > n.Parent().End()) {
			panic(fmt.Sprintf("Node of kind %T is not a subspan of its parent of kind %T", n, n.Parent()))
		}

		var argumentInfo = getImmediatelyContainingArgumentInfo(n, position, sourceFile)
		if argumentInfo != nil {
			return argumentInfo
		}

		// TODO: Handle generic call with incomplete syntax
	}

	return nil
}

func getChildListThatStartsWithOpenerToken(parent ast.Node, openerToken ast.Node, sourceFile *ast.SourceFile) ast.Node {
	var children = getNodeChildren(parent, sourceFile)
	var indexOfOpenerToken = nodeIndexOf(children, openerToken)
	assert(indexOfOpenerToken >= 0 && len(children) > indexOfOpenerToken + 1)
	return children[indexOfOpenerToken + 1]
}

func createSignatureHelpItems(candidates []*checker.Signature, signature *checker.Signature, argumentInfo *ArgumentListInfo, typeChecker checker.TypeChecker) *SignatureHelpItems {
	var items []SignatureInformation

	for _, candidate := range candidates {
		var name = "("

		var parameters []ParameterInformation
		for i, parameter := range candidate.Parameters {
			var tpe = typeChecker.GetTypeOfSymbol(candidate.Parameters[i])
			var typeName = typeChecker.TypeToString(tpe, nil, true)
			if i > 0 {
				name += ", "
			}

			var label = parameter.Name + " "+ typeName
			parameters = append(parameters, ParameterInformation{
				Label:         parameter.Name,
				Documentation: "",
			})

			name += label
		}
		name = name + ")"

		items = append(items, SignatureInformation{
			Label:           name,
			Documentation:   name,
			Parameters:      parameters,
			ActiveParameter: 0,
		})
	}

	if len(items) > 0 {
		return &SignatureHelpItems{
			Items:             items,
			ApplicableSpan:    argumentInfo.ArgumentsSpan,
			SelectedItemIndex: 0,
			ArgumentIndex:     argumentInfo.ArgumentIndex,
			ArgumentCount:     argumentInfo.ArgumentCount,
		}
	}

	return nil
}

